package jtracking_server;

import jtracking_both.*;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.apache.log4j.*;

/**
 *  empfängt die daten vom Client, dekodiert sie und übergibt sie an {@link DBManager}.
 *  nutzt einen ServerSocket
 */
// <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
// #[regen=yes,id=DCE.EC173090-31D2-15C5-78D7-FCEF717F7362]
// </editor-fold> 
public class Datenempfänger extends Thread {

    // <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
    // #[regen=yes,id=DCE.A93EE34A-0B8A-E0AE-1841-E0F7D3B96E2C]
    // </editor-fold> 
    private ServerSocket serverSocket;
    private int timeout=3000;
    private boolean empfangen=true;
    private boolean beenden=false;
    private int port=58722;

    private Main main=Main.main;
    private final int protocolVersion = 1;

    private final File einstellungsOrdner = new File("C:\\Programme\\jtracking\\");
    private final File einstellungsDatei = new File(einstellungsOrdner,"jtracking-server-einstellungen.conf");

    private final Logger logger = Logger.getLogger(this.getClass());
    /**
     * Das KeyPair des Servers. Wird beim Starten eingelesen.
     */

    // <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
    // #[regen=yes,id=DCE.3654C2D7-0DB3-7A28-0951-D8B4548A24A4]
    // </editor-fold> 
    protected Datenempfänger (int port) {
        if (port > 65536 || port < 1)
        {
            //ports gehen nur von 1 bist 65535
            port=this.port;
        }
        this.port=port;

        //datenempfänger initialisieren

        einstellungenLesen();

    }

    private void einstellungenSpeichern()
    {
        logger.info("Speichere Einstellungen und PrivateKey");
        einstellungsOrdner.mkdirs();
        try {
            einstellungsDatei.createNewFile();
        } catch (IOException ex) {
            logger.error("Konnte die Speicherdatei nicht erstellen",ex);
        }
        if(einstellungsDatei.canWrite()) {
            ObjectOutputStream oos = null;
            try {
                oos = new ObjectOutputStream(new FileOutputStream(einstellungsDatei));
                oos.writeInt(protocolVersion);
                oos.writeInt(port);
                oos.writeInt(timeout);
            } catch (FileNotFoundException ex) {
                logger.error("Die Speicherdatei existiert nicht.",ex);
            } catch (IOException ex) {
                logger.error("I-O-Error beim Speichern der Einstellungen",ex);
            } finally {
                try {
                    oos.flush();
                    oos.close();
                    oos=null;
                } catch (IOException ex) {
                    logger.error("I-O-Error beim Speichern der Einstellungen",ex);
                }
            }
        } else {
            logger.error("Konnte die Einstellungen nicht speichern, da keine Schreibrechte vorhanden sind.");
        }
    }

    private void einstellungenLesen()
    {

        ObjectInputStream is=null;
        if(einstellungsDatei.canRead()) {
            logger.info("Lese Einstellungen");
            try {
                is = new ObjectInputStream(new FileInputStream(einstellungsDatei));

                int protV = is.readInt();
                if(protV != protocolVersion)
                {
                    logger.error("Einstellungen konnten nicht geladen werden, da die Speicherdatei von einer anderen Version ist");
                }

                port = is.readInt();
                timeout= is.readInt();

            } catch (FileNotFoundException ex) {
                ex.printStackTrace();
            } catch (IOException ex) {
                logger.error("I-O-Error beim Lesen der Einstellungen",ex);
            } finally {
                try {
                    is.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        } else {
        }
        
    }

    @Override
    public void run()
    {
        //Shutdown Hook
        Runtime.getRuntime().addShutdownHook(new Thread(){
            @Override
            public void run()
            {
                beenden();
            }
        });


        boolean socketStarted=false;
        int tries=0;
        do {
            try {
                tries++;
                serverSocket = new ServerSocket(port);
                socketStarted=true;
            } catch (IOException e) {
                logger.error("Konnte bei Versuch "+tries+" nicht auf Port "+port+" starten...",e);
            }
            if(!socketStarted)
            {
                try {
                    sleep(2000);
                } catch (InterruptedException ex) {}
                if(tries>10)
                {
                    socketStarted=true;
                    logger.fatal("FEHLER: Konnte nicht auf Port "+port+" binden. Programm wird beendet!");
                    System.exit(-1);
                }
            }
        } while(!socketStarted);
        
        ExecutorService executor=Executors.newCachedThreadPool();
        while(!beenden)
        {
            try{
                if(empfangen)
                {
                    Socket clientSocket = null;
                    try {
                        clientSocket = serverSocket.accept();
                        executor.execute(new ClientConnection(clientSocket));

                    } catch (IOException e) {logger.error("Ungültige Verbindung von Client, ignoriert.",e);}


                } else {
                    //ich warte auf eine wiederbelebung...
                    try{
                        synchronized(this){
                            wait();
                        }
                    } catch (InterruptedException e) {}
                }

                //ich will eine InterruptedException auffangen
                if(false)
                    throw new InterruptedException();
            } catch(InterruptedException e) {}
        }

        if(serverSocket!=null)
        {
            try {
                serverSocket.close();
            } catch (IOException ex) {
                ex.getLocalizedMessage();
            }
            serverSocket=null;
        }

        logger.info("Beenden-Kommando erhalten. Es werden keine neuen Verbindungen mehr angenommen.\n" +
                "Laufende verbindungen werden in 15 Sekunden geschlossen.");
        executor.shutdown();
        boolean alleBeendet=false;
        try {
            alleBeendet=executor.awaitTermination(15, TimeUnit.SECONDS);
        } catch (InterruptedException ex) {}
        if(!alleBeendet)
            logger.info("Es wurden nicht alle Übertragungen während der Wartezeit fertiggestellt, diese werden jetzt gekappt.");
        logger.info("JTracking_server wird jetzt beendet.");
        System.exit(0);
    }

    /**
     * hört auf zu empfangen
     * Achtung: gerade stattfindende Übertragungen könnten unterbrochen werden!
     * @see wiederEmpfangen()
     */
    protected void nichtMehrEmpfangen()
    {
        empfangen=false;
        this.interrupt();
    }

    /**
     * versucht wieder, zu empfangen
     * @see nichtMehrEmpfangen()
     */
    protected void wiederEmpfangen()
    {
        empfangen=true;
        synchronized(this){
            this.notify();
        }
    }

    /**
     * Beendet den Server, stellt laufende Übertragungen fertig und beendet dann JTracking_server.
     * nur Ausführen, wenn zum Beenden bereit!
     * Achtung: gerade stattfindende Übertragungen könnten unterbrochen werden!
     *
     * @see nichtMehrEmpfangen()
     */
    protected void beenden()
    {
        
        //Keys Speichern
        einstellungenSpeichern();
        //nicht mehr empfangen
        empfangen=false;
        beenden=true;
        synchronized(this){
            this.notify();
        }
        this.interrupt();
    }



    private class ClientConnection extends Thread {
        Socket clientSocket;
        TimeOutThread tot;
        ClientConnection(Socket clientSocket)
        {
            this.clientSocket=clientSocket;

            //ClientConnection initialisieren

        }

        @Override
        public void run()
        {
            logger.info("Neue Verbindung: Client "+clientSocket.getInetAddress()+" auf Port "+clientSocket.getPort());
            if(clientSocket.isClosed())
            {
                logger.error("Fehler beim Annehmen einer Verbindung");
                return;
            }
            try {
                try{
                    waiting(true);
                    //daten empfangen und an DBManager weitergeben
                    input = new ObjectInputStream(clientSocket.getInputStream());
                    waiting(false);
                } catch (EOFException e) {
                    logger.info("Verbindung "+clientSocket.getInetAddress()+" "+clientSocket.getPort()+" war ein Ping und wird beendet.");
                    return;
                }
                waiting(true);
                //output = new OutputStreamWriter(socket.getOutputStream());
                output = new ObjectOutputStream(clientSocket.getOutputStream());
                output.flush();
                waiting(false);

                waiting(true);
                int awaitedPackets=input.readInt();
                waiting(false);

                HashSet<Datenpaket> empfangenePakete =null;
                try{
                    Object obj= input.readObject();
                    if(obj instanceof HashSet){
                        empfangenePakete=new HashSet<Datenpaket>((HashSet)obj);
                    } else {
                        logger.error("Client hat ein nicht erwartetes Objekt gesendet: Klasse: "
                                +obj.getClass().getName()+"; toString: "+obj.toString());
                    }
                } catch (ClassNotFoundException ex) {
                    logger.error("Client scheint eine falsche Version zu haben, konnte ein Datenpaket nicht empfangen",ex);
                }
                waiting(false);

                logger.info(empfangenePakete.size()+" Pakete wurden empfangen, "+
                        (awaitedPackets-empfangenePakete.size())+
                        " wurden nicht empfangen, aber erwartet.");

                for(Datenpaket p:empfangenePakete)
                {
                    boolean b=Main.main.dBManager.paketEintragen(p);

                    if(b)
                        logger.debug("Ein Paket "+ "konnte" + " eingetragen werden: "+p.toString());
                    else
                        logger.error("Ein Paket "+ "konnte NICHT" + " eingetragen werden!");
                }
                empfangenePakete.clear();


            } catch (java.net.SocketException ex) {
                if(ex.getMessage().equals("Connection reset"))
                    logger.info("Verbindung  mit "+clientSocket.getInetAddress()+" "+clientSocket.getLocalPort()+" abgebrochen.");
                else
                    logger.error("Verbindungsfehler",ex);
            } catch (IOException ex) {
                ex.printStackTrace();
            } finally {
                //verbindungen schließen
                timeout(false);
            }
        }


    private void waiting(boolean b)
    {
        if(tot!=null)
        {
            tot.run=false;
            tot.interrupt();
        }
        if(b){
            tot=new TimeOutThread();
            tot.setName(this.getName()+"-tot");
            tot.setPriority(Math.round((Thread.MAX_PRIORITY+Thread.NORM_PRIORITY)/2));
            tot.start();
        }
    }

        /**
         * Schließt die verbindung, um den Thread zu entblocken.
         * @param timeout ob das ein timeout oder ein normales beenden ist
         */
        private void timeout(boolean timeout)
        {
            
            waiting(false);
            try {
                logger.info((timeout? "Timeout - ":"")
                        +"Verbindung mit "+clientSocket.getInetAddress()+" "+
                        clientSocket.getPort()+" wird beendet");
                output.flush();
                input.close();
                output.close();
                clientSocket.close();
                input=null;
                output=null;
                clientSocket=null;
                if(tot!=null)
                {
                    tot.run=false;
                    tot.interrupt();

                }
            } catch (Exception ex) {
            }
            System.gc();
        }

        //utility functions
        // <editor-fold defaultstate="collapsed" desc="utility functions">

        /**
         * timeOutThread
         * sorgt für einen timeout, wenn nix mehr ankommt
         */
        class TimeOutThread extends Thread{
            boolean run=true;
        @Override
        public void run()
        {
            try {
                sleep(Datenempfänger.this.timeout);
            } catch (InterruptedException ex) {}
            if(run){
                logger.debug("Meep, timeout");
                ClientConnection.this.timeout(true);
            }
        }
        }

        /**
         * very fast hash function
         * @param str
         * @return
         */
        public final int sDBMHash(String str) {
            return sDBMHash(str.getBytes());
        }

        public final int sDBMHash(byte[] b){
            int n = 0;
            int len = b.length;

            for (int i=0;i<len;i++) {
                n = b[i] + 65599 *n;
            }
            return n;
        }

        

        //</editor-fold>

        private boolean waiting=false;

        private ObjectInputStream input=null;
        private ObjectOutputStream output=null;


    }


    /*
     * Format für Übertragung:
     * alles über ObjectOutputStream
     * Server sagt "jtracking\n"
     * Server sagt int mit hashCode seines PubKeys
     * Client sagt boolean, ob er den pubkey braucht
     * Server schickt pubkey oder nicht
     *
     * jetzt wird alles verschlüsselt gesendet:
     * Client sendet dem Server seinen PubKey
     * server sagt "numberofpackets"
     * Client gibt zahl der zu versendenden Pakete als int
     * Client schickt alle Pakete als Object
     * Server sagt "gotallbye"
     * Server beendet verbindung
     *
     * //client hat pubkey, um eventuell später noch einen chat zu implementieren
     */


    

    
}

